const fs = require('fs')

const VectorIndexSize = 8
const VectorIndexCols = 256
const VectorIndexLength = 256 * 256 * (4 + 4)
const SegmentIndexSize = 14
const IP_REGEX = /^((\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])\.){3}(\d|[1-9]\d|1\d\d|2[0-4]\d|25[0-5])$/

function getStartEndPtr(idx, fd, ioStatus) {
  if (this._vectorIndex) {
    const sPtr = this._vectorIndex.readUInt32LE(idx)
    const ePtr = this._vectorIndex.readUInt32LE(idx + 4)
    return { sPtr, ePtr }
  } else {
    return new Promise((resolve, reject) => {
      ioStatus.ioCount += 1
      fs.read(fd, Buffer.alloc(8), 0, 8, 256 + idx, (err, bytesRead, buf) => {
        if (err) {
          reject(err)
        } else {
          resolve({ sPtr: buf.readUInt32LE(0), ePtr: buf.readUInt32LE(4) })
        }
      })
    })
  }
}

function getBuffer(offset, length, fd, ioStatus) {
  if (this._buffer) {
    return this._buffer.slice(offset, offset + length)
  } else {
    return new Promise((resolve, reject) => {
      ioStatus.ioCount += 1
      fs.read(fd, Buffer.alloc(length), 0, length, offset, (err, bytesRead, buf) => {
        if (err) {
          reject(err)
        } else {
          resolve(buf)
        }
      })
    })
  }
}

function openFilePromise(fileName) {
  return new Promise((resolve, reject) => {
    fs.open(fileName, 'r', (err, fd) => {
      if (err) {
        reject(err)
      } else {
        resolve(fd)
      }
    })
  })
}

class Searcher {
  constructor(dbFile, vectorIndex, buffer) {
    this._dbFile = dbFile
    this._vectorIndex = vectorIndex
    this._buffer = buffer

    if (this._buffer) {
      this._vectorIndex = this._buffer.slice(256, 256 + VectorIndexLength)
    }
  }

  search(ip) {
    const startTime = process.hrtime()
    const ioStatus = { ioCount: 0 }

    if (!isValidIp(ip)) {
      throw new Error(`IP: ${ip} is invalid`)
    }

    let fd = -1

    if (!this._buffer) {
      fd = openFilePromise(this._dbFile).catch((err) => {
        throw err
      })
    }

    const ps = ip.split('.')
    const i0 = parseInt(ps[0])
    const i1 = parseInt(ps[1])
    const i2 = parseInt(ps[2])
    const i3 = parseInt(ps[3])

    const ipInt = i0 * 256 * 256 * 256 + i1 * 256 * 256 + i2 * 256 + i3
    const idx = i0 * VectorIndexCols * VectorIndexSize + i1 * VectorIndexSize
    const { sPtr, ePtr } = getStartEndPtr.call(this, idx, fd, ioStatus)

    let l = 0
    let h = (ePtr - sPtr) / SegmentIndexSize
    let result = null

    while (l <= h) {
      const m = (l + h) >> 1

      const p = sPtr + m * SegmentIndexSize

      const buff = getBuffer.call(this, p, SegmentIndexSize, fd, ioStatus)

      const sip = buff.readUInt32LE(0)

      if (ipInt < sip) {
        h = m - 1
      } else {
        const eip = buff.readUInt32LE(4)
        if (ipInt > eip) {
          l = m + 1
        } else {
          const dataLen = buff.readUInt16LE(8)
          const dataPtr = buff.readUInt32LE(10)
          const data = getBuffer.call(this, dataPtr, dataLen, fd, ioStatus)
          result = data.toString('utf-8')
          break
        }
      }
    }
    if (fd) {
      fs.close(fd, function () {})
    }

    const diff = process.hrtime(startTime)

    const took = (diff[0] * 1e9 + diff[1]) / 1e3
    return { region: result, ioCount: ioStatus.ioCount, took }
  }
}

function _checkFile(dbPath) {
  try {
    fs.accessSync(dbPath, fs.constants.F_OK)
  } catch (err) {
    throw new Error(`${dbPath} ${err ? 'does not exist' : 'exists'}`)
  }

  try {
    fs.accessSync(dbPath, fs.constants.R_OK)
  } catch (err) {
    throw new Error(`${dbPath} ${err ? 'is not readable' : 'is readable'}`)
  }
}

function isValidIp(ip) {
  return IP_REGEX.test(ip)
}

function newWithFileOnly(dbPath) {
  _checkFile(dbPath)

  return new Searcher(dbPath, null, null)
}

function newWithVectorIndex(dbPath, vectorIndex) {
  _checkFile(dbPath)

  if (!Buffer.isBuffer(vectorIndex)) {
    throw new Error('vectorIndex is invalid')
  }

  return new Searcher(dbPath, vectorIndex, null)
}

function newWithBuffer(buffer) {
  if (!Buffer.isBuffer(buffer)) {
    throw new Error('buffer is invalid')
  }

  return new Searcher('', null, buffer)
}

function loadVectorIndexFromFile(dbPath) {
  const fd = fs.openSync(dbPath, 'r')
  const buffer = Buffer.alloc(VectorIndexLength)
  fs.readSync(fd, buffer, 0, VectorIndexLength, 256)
  fs.close(fd, function () {})
  return buffer
}

function loadContentFromFile(dbPath) {
  const stats = fs.statSync(dbPath)
  const buffer = Buffer.alloc(stats.size)
  const fd = fs.openSync(dbPath, 'r')
  fs.readSync(fd, buffer, 0, stats.size, 0)
  fs.close(fd, function () {})
  return buffer
}

const SearchMode = {
  File: 'FILE',
  VectorIndex: 'VECTOR_INDEX',
  Full: 'FULL'
}

const config = {
  xdbPath: 'src/shared/ip2region/ip2region.xdb',
  mode: SearchMode.VectorIndex
}

const ip2region = {
  isValidIp,
  loadVectorIndexFromFile,
  loadContentFromFile,
  newWithFileOnly,
  newWithVectorIndex,
  newWithBuffer,
  SearchMode,
  config
}

module.exports = ip2region
